Vue3 中配合 TypeScript 使用 Vuex
使用 TS 的问题
在 TypeScript 中使用 Vuex,必须编写自己的类型声明,这篇笔记主要记录如何使用(官方文档 TypeScript 部分根本就没有写)
State
定义一个 State 的类型 state.ts
export const state = {
counter: 0,
}
export type State = typeof state
这里需要导出类型信息和 state
Mutations
这里可以将这个 Mutations 定义成枚举类型 mutation-types.ts
export enum MutationTypes {
SET_COUNTER = 'SET_COUNTER',
}
现在我们已经定义了 Mutations 的名称,可以为每个 mutations(其实际类型)声明一个属性名。
Vuex 提供的 Mutation 函数,它接受 State 作为第一个参数,而 payload 作为第二个参数,
定义好枚举后,就可以开始定义真正的 mutation 了
mutations.ts
:
import { MutationTree } from 'vuex'
import { MutationTypes } from './mutation-types'
import { State } from './state'
// 可以注意到,这里的 State 就自己定义的 State 类型
export type Mutations<S = State> = {
[MutationTypes.SET_COUNTER](state: S, payload: number): void
}
export const mutations: MutationTree<State> & Mutations = {
[MutationTypes.SET_COUNTER](state, payload: number) {
state.counter = payload
},
}
这个 mutation 可以用来修改上面 State 里面定义的 counter
属性,这里的 MutationTree<State> & Mutations
交叉属性可以保证 TypeScript 能正常编译,否则会抛出错误:
Type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' is not assignable to type 'MutationTree<{ counter: number; }> & Mutations<{ counter: number; }>'.
Property '[MutationTypes.RESET_COUNTER]' is missing in type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' but required in type 'Mutations<{ counter: number; }>'
它是一个泛型类型,与包一起提供。从它的名字来看,很明显,它有助于声明一个 MutationTree 类型(说白了就是另一种形式的继承接口..)
看它的实现:
vuex/types/index.d.ts
:
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
它的作用就是兼容自己写的 State
Actions
对于这样一个简单的存储区,不需要 Actions 操作,但是为了说明操作的类型,让我们假设我们可以从某处获取 counter。
就像上面的 mutations 那样,这里也定义一个枚举 action-types.ts
export enum ActionTypes {
GET_COUTNER = 'GET_COUTNER',
}
actions.ts
:
import { ActionTypes } from './action-types'
export const actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}
这里有一个简单的返回操作,它延时 500毫秒返回,但是这里也会像上面 mutations 那样报错:
所以这里也需要像上面那样使用交叉类型
import { ActionTree, ActionContext } from 'vuex'
import { State } from './state'
import { Mutations } from './mutations'
import { ActionTypes } from './action-types'
import { MutationTypes } from './mutation-types'
type AugmentedActionContext = {
commit<K extends keyof Mutations>(
key: K,
payload: Parameters<Mutations[K]>[1]
): ReturnType<Mutations[K]>
} & Omit<ActionContext<State, State>, 'commit'>
export interface Actions {
[ActionTypes.GET_COUTNER](
{ commit }: AugmentedActionContext,
payload: number
): Promise<number>
}
export const actions: ActionTree<State, State> & Actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}
Getters
Getters 也可以静态类型化。Getter 就像 mutation,本质上是一个接收状态作为第一个参数的函数。getters 的声明与 Actions 的声明没有太大区别。
getters.ts
import { GetterTree } from 'vuex'
import { State } from './state'
export type Getters = {
doubledCounter(state: State): number
}
export const getters: GetterTree<State, State> & Getters = {
doubledCounter: (state) => {
return state.counter * 2
},
}
拓充进全局类型 $store
现在把上面写的注册全局类型 $store
中
store.ts
import {
createStore,
Store as VuexStore,
CommitOptions,
DispatchOptions,
} from 'vuex'
import { State, state } from './state'
import { Getters, getters } from './getters'
import { Mutations, mutations } from './mutations'
import { Actions, actions } from './actions'
export const store = createStore({
state,
getters,
mutations,
actions,
})
export type Store = Omit<
VuexStore<State>,
'getters' | 'commit' | 'dispatch'
> & {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload: P,
options?: CommitOptions
): ReturnType<Mutations[K]>
} & {
dispatch<K extends keyof Actions>(
key: K,
payload: Parameters<Actions[K]>[1],
options?: DispatchOptions
): ReturnType<Actions[K]>
} & {
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>
}
}
我们已经到达了终点,剩下的就是全局 Vue 类型的扩展。(可以省略)
types/index.d.ts
import { Store } from '../store'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store
}
}
简单使用测试
这里有两种使用方式:Options API 和新的 Composition API
下面分别介绍:
Options API
<template>
<section>
<h2>Options API Component</h2>
<p>Counter: {{ counter }}, doubled counter: {{ counter }}</p>
<input v-model.number="counter" type="text" />
<button type="button" @click="resetCounter">Reset counter</button>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'
export default defineComponent({
name: 'OptionsAPIComponent',
computed: {
counter: {
get() {
return this.$store.state.counter
},
set(value: number) {
this.$store.commit(MutationTypes.SET_COUNTER, value)
},
},
doubledCounter() {
return this.$store.getters.doubledCounter
}
},
methods: {
resetCounter() {
this.$store.commit(MutationTypes.SET_COUNTER, 0)
},
async getCounter() {
const result = await this.$store.dispatch(ActionTypes.GET_COUTNER, 256)
},
},
})
</script>
Composition API
要在使用 Composition API 定义的组件中使用存储,我们必须通过钩子访问它,钩子只返回存储: useStore
export function useStore() {
return store as Store
}
<script lang="ts">
import { defineComponent, computed, h } from 'vue'
import { useStore } from '../store'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'
export default defineComponent({
name: 'CompositionAPIComponent',
setup(props, context) {
const store = useStore()
const counter = computed(() => store.state.counter)
const doubledCounter = computed(() => store.getters.doubledCounter)
function resetCounter() {
store.commit(MutationTypes.SET_COUNTER, 0)
}
async function getCounter() {
const result = await store.dispatch(ActionTypes.GET_COUTNER, 256)
}
return () =>
h('section', undefined, [
h('h2', undefined, 'Composition API Component'),
h('p', undefined, counter.value.toString()),
h('button', { type: 'button', onClick: resetCounter }, 'Reset coutner'),
])
},
})
</script>
模块化
import { createApp } from 'vue'
import { createStore } from 'vuex'
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
export default createStore({
modules: {
a: moduleA,
b: moduleB
}
})
在 Vue3 中使用 mapXXX 的问题
在 Vue3 中使用 mapGetters 时会发生报错,经排查,发现在 Vue3 中的 composition api 是无法使用 mapGetters的,因为它依赖于 this
具体参考这个 issue https://github.com/vuejs/vuex/issues/1948
所以下面这种写法用不了
const store = useStore();
const KeyGetters = computed(() => {
return {
...mapGetters([
'keyboard/isRecall'
// ...
])
};
});
得手动添加
const store = useStore();
const KeyGetters = computed(() => {
return {
isRecall: store.getters['keyboard/isRecall'],
selectKeys: store.getters['keyboard/selectKeys'],
pressedKeys: store.getters['keyboard/selectPressedKeys']
};
});
Reference
Vue 3, Vuex 4 Modules, Typescript(主要看这篇博客) 官方文档 TypeScript 支持 Vuex + TypeScript